package jmine.tec.web.servlet.cipher;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;

import javax.crypto.Cipher;

import jmine.tec.utils.collection.MultiMap;
import jmine.tec.utils.collection.impl.ListMultiMap;
import jmine.tec.utils.cripto.cipher.CipherInputStream;
import jmine.tec.utils.cripto.cipher.CipherOutputStream;
import jmine.tec.utils.cripto.key.GeradorChaveRSA;
import jmine.tec.utils.cripto.key.KeyPairHolder;
import jmine.tec.utils.cripto.key.PublicKeyLocator;
import jmine.tec.utils.cripto.key.impl.DynamicKeyPairHolder;
import jmine.tec.utils.io.http.Connector;
import jmine.tec.utils.io.http.exception.HttpClientErrorException;
import jmine.tec.utils.io.http.exception.HttpException;
import jmine.tec.utils.io.http.exception.RetryRequestException;
import jmine.tec.utils.io.http.impl.URLConnector;
import bancosys.tec.utils.md5sum.MD5SumTool;

/**
 * Extende de {@link URLConnector} implementando o protocolo de comunicacao com negociacao de chaves assimetricas
 * 
 * @author takeshi
 */
public class SecureCommunicationConnector extends URLConnector implements Connector, SecureCommunicationProtocol {

    private static final MultiMap<String, String> EMPTY_QUERY_PARAMETERS = new ListMultiMap<String, String>();

    private final Key originalDecodeKey;

    private final String keyPath;

    private final PublicKeyLocator keyLocator;

    private final String serverCertificateName;

    private final Cipher cipher;

    private final int chunkSize;

    private volatile Key decodeKey;

    private volatile Key encodeKey;

    private volatile int status;

    /**
     * Construtor
     * 
     * @param baseURL a url basica
     * @param privateKey a chave privada
     * @param keyPath o caminho da chave
     * @param cipher o {@link Cipher}
     * @param chunkSize int
     * @param keyLocator the {@link PublicKeyLocator}
     * @param serverCertificateName the alias for the server certificate
     */
    public SecureCommunicationConnector(String baseURL, Key privateKey, String keyPath, Cipher cipher, int chunkSize,
            PublicKeyLocator keyLocator, String serverCertificateName) {
        super(baseURL);
        if (privateKey == null || serverCertificateName == null || keyLocator == null || keyPath == null || cipher == null) {
            throw new NullPointerException();
        }
        if (chunkSize <= 0) {
            throw new IllegalStateException();
        }
        this.serverCertificateName = serverCertificateName;
        this.keyLocator = keyLocator;
        this.decodeKey = privateKey;
        this.originalDecodeKey = this.decodeKey;
        this.keyPath = keyPath;
        this.cipher = GeradorChaveRSA.copyCipher(cipher);
        this.chunkSize = chunkSize;
    }

    /**
     * Construtor
     * 
     * @param baseURL a url basica
     * @param holder the KeyPairHolder
     * @param keyPath o caminho da chave
     * @param cipher o {@link Cipher}
     * @param chunkSize int
     * @param keyLocator the {@link PublicKeyLocator}
     * @param serverCertificateName the alias for the server certificate
     */
    public SecureCommunicationConnector(String baseURL, KeyPairHolder holder, String keyPath, Cipher cipher, int chunkSize,
            PublicKeyLocator keyLocator, String serverCertificateName) {
        this(baseURL, holder.get().getPrivate(), keyPath, cipher, chunkSize, keyLocator, serverCertificateName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized OutputStream wrapOutputStream(OutputStream output) throws IOException {
        if (this.encodeKey == null) {
            return super.wrapOutputStream(output);
        } else {
            try {
                return new CipherOutputStream(output, GeradorChaveRSA.copyCipher(this.cipher), this.encodeKey, this.chunkSize);
            } catch (InvalidKeyException e) {
                throw new IOException(e.getMessage());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized InputStream getInputStream(HttpURLConnection connection) throws IOException {
        if (decodeKey != null) {
            try {
                return new CipherInputStream(connection.getInputStream(), GeradorChaveRSA.copyCipher(this.cipher), this.decodeKey);
            } catch (InvalidKeyException e) {
                throw new IOException(e.getMessage());
            }
        }
        return connection.getInputStream();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized HttpURLConnection openConnection(String path, MultiMap<String, String> queryParams) throws IOException,
            HttpException {
        if (this.status == STARTED) {
            this.performHandshake(path);
        }
        return super.openConnection(path, queryParams);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized void handleClientError(int statusCode, String statusText, String path, MultiMap<String, String> params,
            String errorBody) throws HttpClientErrorException {
        if (statusCode == SecureCommunicationProtocol.INITIALIZATION_NEEDED) {
            resetCommunicationStatus();
            throw RetryRequestException.SHARED_INSTANCE;
        }
        super.handleClientError(statusCode, statusText, path, params, errorBody);
    }

    /**
     * Inicia um handshake para negociar as chaves de comunicacao
     * 
     * @param path String
     * @throws IOException e
     * @throws HttpException e
     */
    private synchronized void performHandshake(String path) throws IOException, HttpException {
        InputStream inputStream = this.startHandshake(path);
        ObjectInputStream oin = new ObjectInputStream(inputStream);
        try {
            HttpURLConnection connection = this.respondAssertion(path, oin);
            try {
                // it should now have the real encode key
                this.encodeKey = GeradorChaveRSA.leChavePublica(this.getInputStream(connection));
            } finally {
                connection.disconnect();
            }
            this.negotiateCommunicationKeys(path);
        } catch (ClassNotFoundException e) {
            this.encodeKey = null;
            throw new IOException(e.getMessage());
        } catch (InvalidKeySpecException e) {
            this.encodeKey = null;
            throw new IOException(e.getMessage());
        } finally {
            inputStream.close();
        }
    }

    /**
     * Inicia o handshake, fazendo um request mandando o keyPath e com um parametro extra
     * 
     * @param path o path
     * @return {@link InputStream}
     * @throws IOException e
     * @throws HttpException e
     */
    private InputStream startHandshake(String path) throws IOException, HttpException {
        this.status = NEGOTIATING;
        HashMap<String, String> params = new HashMap<String, String>();
        params.put(SecureCommunicationProtocol.KEY_PATH_PARAMETER_NAME, this.keyPath);
        params.put(SecureCommunicationProtocol.INITIALIZATION_PARAMETER_NAME, "true");
        InputStream inputStream = this.makeGet(path, params);
        return inputStream;
    }

    /**
     * Le a assertion do ObjectInputStream passado, calcula a resposta e devolve em um request novo
     * 
     * @param path o path
     * @param oin o ObjectInputStream
     * @return {@link HttpURLConnection} de onde ler a chave nova
     * @throws IOException e
     * @throws ClassNotFoundException nao deveria ser lancado nunca
     * @throws HttpException e
     */
    private HttpURLConnection respondAssertion(String path, ObjectInputStream oin) throws IOException, ClassNotFoundException,
            HttpException {
        String question = (String) oin.readObject();
        this.encodeKey = this.keyLocator.getPublicKey(this.serverCertificateName);
        String answer = MD5SumTool.md5Sum(question);

        HttpURLConnection connection = this.openConnection(path, EMPTY_QUERY_PARAMETERS);
        this.setCookies(connection);
        connection.setDoOutput(true);
        OutputStream output = this.wrapOutputStream(connection.getOutputStream());
        output.write(answer.getBytes("UTF-8"));
        output.flush();
        // now negociate the keys to actually do the communication
        this.handleResponse(path, EMPTY_QUERY_PARAMETERS, connection, this.extractResponseHeaderFields(connection));
        return connection;
    }

    /**
     * Cria um novo par de chaves que deve ser usado a partir desse momento
     * 
     * @param path String
     * @throws IOException e
     * @throws HttpException e
     */
    private void negotiateCommunicationKeys(String path) throws IOException, HttpException {
        HttpURLConnection connection = this.openConnection(path, EMPTY_QUERY_PARAMETERS);
        Closeable closeable = null;
        try {
            KeyPair keyPair = new DynamicKeyPairHolder().get();
            this.decodeKey = keyPair.getPrivate();
            this.setCookies(connection);
            connection.setDoOutput(true);
            // now a new connection just to send our encode key
            OutputStream output2 = this.wrapOutputStream(connection.getOutputStream());
            closeable = output2;
            output2.write(keyPair.getPublic().getEncoded());
            output2.flush();
            this.handleResponse(path, EMPTY_QUERY_PARAMETERS, connection, this.extractResponseHeaderFields(connection));
        } finally {
            connection.disconnect();
            if (closeable != null) {
                closeable.close();
            }
        }
    }

    /**
     * @return String
     */
    public String getServerCertificateName() {
        return this.serverCertificateName;
    }

    /**
     * 
     */
    protected void resetCommunicationStatus() {
        this.status = STARTED;
        this.decodeKey = this.originalDecodeKey;
        this.encodeKey = null;
    }

    /**
     * {@inheritDoc}
     */
    public int getStatus() {
        return status;
    }

}
